﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;

namespace Ooaavee.TestTools
{
    /// <summary>
    /// This factory class creates test hooks.
    /// </summary>
    internal static class TestHookFactory
    {

        #region Private fields

        private static readonly Dictionary<Type, Type> TypeCache = new Dictionary<Type, Type>();
        private static readonly object Lock = new object();

        #endregion

        internal delegate void NewHandleCallback(TestHookHandle handle);

        /// <summary>
        /// Creates test hooks for the specified test method and test hook category.
        /// </summary>
        /// <param name="testMethod">test method</param>
        /// <param name="category">test hook category</param>
        /// <param name="context">ExtendedTestContext</param>
        /// <param name="callback">callback</param>
        internal static void CreateTestHooks(MethodInfo testMethod, TestHookCategory category, ExtendedTestContext context, NewHandleCallback callback)
        {
            foreach (TestHookDefinitionAttribute definition in GetDefinitions(testMethod, category))
            {
                ITestHook testHook = CreateInstance(definition.InterfaceType);
                TestHookHandle handle = new TestHookHandle(testHook, definition.Parameters, context);
                callback(handle);
            }
        }

        /// <summary>
        /// Gets all test hook definitions for the specified test method and test hook category.
        /// </summary>
        /// <param name="testMethod">test method</param>
        /// <param name="category">test hook category</param>
        /// <returns>definitions</returns>
        private static IEnumerable<TestHookDefinitionAttribute> GetDefinitions(MethodInfo testMethod, TestHookCategory category)
        {
            var definitions = new List<TestHookDefinitionAttribute>();

            if (IgnoreInheritedHooks(testMethod) == false)
            {
                var x = testMethod.DeclaringType.GetCustomAttributes(typeof (TestHookDefinitionAttribute), true) as TestHookDefinitionAttribute[];
                if (x != null)
                {
                    definitions.AddRange(x);
                }
            }

            var y = testMethod.GetCustomAttributes(typeof(TestHookDefinitionAttribute), true) as TestHookDefinitionAttribute[];
            if (y != null)
            {
                definitions.AddRange(y);
            }

            definitions.Sort((definition1, definition2) => definition1.Order.CompareTo(definition2.Order));

            return definitions.FindAll(definition => definition.Category == category);
        }

        /// <summary>
        /// Checks if the specified test method should ignore all inherited <see cref="RunBeforeTestAttribute"/>, <see cref="RunAfterTestAttribute"/>, 
        /// <see cref="RunAfterPassedTestAttribute"/> and <see cref="RunAfterFailedTestAttribute"/> attributes.
        /// </summary>
        /// <param name="testMethod">test method</param>
        /// <returns>true/false</returns>
        private static bool IgnoreInheritedHooks(MethodInfo testMethod)
        {
            IgnoreInheritedTestHooksAttribute[] attributes = testMethod.GetCustomAttributes(typeof (IgnoreInheritedTestHooksAttribute), true) as IgnoreInheritedTestHooksAttribute[];
            return attributes != null && attributes.Length > 0;
        }

        /// <summary>
        /// Creates an instance of test hook.
        /// </summary>
        /// <param name="interfaceType">interface types</param>
        /// <returns>test hook</returns>
        private static ITestHook CreateInstance(Type interfaceType)
        {
            Type implementationType = GetImplementationType(interfaceType);
            object instance = Activator.CreateInstance(implementationType);
            return (ITestHook) instance;
        }

        /// <summary>
        /// Gets the implementation type for the specified interface type.
        /// </summary>
        /// <param name="interfaceType">interface type</param>
        /// <returns>implementation type</returns>
        private static Type GetImplementationType(Type interfaceType)
        {
            Type implementationType;

            lock (Lock)
            {
                if (TypeCache.TryGetValue(interfaceType, out implementationType) == false)
                {
                    string interfaceTypeName = interfaceType.FullName;
                    string implementationTypeName;

                    if (TryGetImplementationTypeName(interfaceTypeName, out implementationTypeName) == false)
                    {
                        throw new TestToolsException(
                            string.Format(CultureInfo.CurrentCulture,
                                          "Could not find the implementation type name for {0}. Please check the configuration file {1}.",
                                          interfaceTypeName, ConfigurationManager.Path));
                    }

                    try
                    {
                        implementationType = Type.GetType(implementationTypeName, true);
                    }
                    catch (Exception ex)
                    {
                        throw new TestToolsException(
                            string.Format(CultureInfo.CurrentCulture,
                                          "Could not find the implementation type named {0}.",
                                          implementationTypeName), ex);
                    }

                    if (IsITestHook(implementationType) == false)
                    {
                        throw new TestToolsException(
                            string.Format(CultureInfo.CurrentCulture,
                                          "The implementation type {0} does not implement ITestHook interface.",
                                          implementationTypeName));
                    }

                    if (HasDefaultConstructor(implementationType) == false)
                    {
                        throw new TestToolsException(
                            string.Format(CultureInfo.CurrentCulture,
                                          "The implementation type {0} does not have a default constructor.",
                                          implementationTypeName));
                    }

                    TypeCache[interfaceType] = implementationType;
                }
            }

            return implementationType;
        }

        /// <summary>
        /// Checks if the specified type implements <see cref="ITestHook"/> interface.
        /// </summary>
        /// <param name="type">type</param>
        /// <returns>true if type implements <see cref="ITestHook"/> interface</returns>
        private static bool IsITestHook(Type type)
        {
            return type.GetInterfaces().Any(i => i.Equals(typeof (ITestHook)));
        }

        /// <summary>
        /// Checks if the specified type has the default constructor.
        /// </summary>
        /// <param name="type">type</param>
        /// <returns>true if the type has the default constructor</returns>
        private static bool HasDefaultConstructor(Type type)
        {
            ConstructorInfo constructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public,
                                                              null,
                                                              CallingConventions.HasThis,
                                                              new Type[0],
                                                              null);
            return constructor != null;
        }

        /// <summary>
        /// Gets the implementation type name for the specified interface type. A return value indicates whether the implementation type 
        /// name was found.
        /// </summary>
        /// <param name="interfaceTypeName">interface type name</param>
        /// <param name="implementationTypeName">implementation type name</param>
        /// <returns>true if implementation type name was found; otherwise, false.</returns>
        private static bool TryGetImplementationTypeName(string interfaceTypeName, out string implementationTypeName)
        {
            foreach (TestToolsConfigurationTestHook configuration in ConfigurationManager.Instance.TestHooks)
            {
                if (configuration.InterfaceTypeName == interfaceTypeName)
                {
                    implementationTypeName = configuration.ImplementationTypeName;
                    return true;                    
                }
            }

            implementationTypeName = null;
            return false;
        }

    }
}